<?php
namespace HC\Swoole;

use HC\Swoole\Socket\Request;
use Psr\Log\LoggerInterface;

use HC\Swoole\Exception\CommandException;
use HC\Swoole\Exception\ServerException;

/**
 * Swoole Server Class
 * @package HC\Swoole
 */
class Server
{
    /**
     * @var string
     */
    protected $server_name;

    /**
     * @var \swoole_server
     */
    protected $server;

    /**
     * @var LoggerInterface
     */
    protected $logger;

    /**
     * @var string
     */
    protected $address = '127.0.0.1';

    /**
     * @var int
     */
    protected $port;

    /**
     * @var int
     */
    protected $timer = 200;

    /**
     * swoole 设置
     * https://wiki.swoole.com/wiki/page/274.html
     * @var array
     */
    protected $options = [
        'worker_num'        => 2,
        'task_worker_num'   => 2,
        'dispatch_mode '    => 2,
        'daemonize'         => true,
        'open_eof_check'    => true,
        'package_eof'       => "\r\n\r\n",
        'log_file'          => '/tmp/hc-sw-%s.log'
    ];

    /**
     * @param string $server_name
     * @param int $port
     * @param array $options
     */
    public function __construct(string $server_name, int $port = 9651, array $options = [])
    {
        if(isset($options['timer']) && intval($options['timer']) > 0) {
            $this->timer = intval($options['timer']);
            unset($options['timer']);
        }

        $this->server_name = $server_name;
        $this->port        = $port;
        $this->options['log_file'] = sprintf($this->options['log_file'], $this->server_name);
        $this->options     = $options ? array_merge($this->options, $options) : $this->options;
        $this->server      = new \swoole_server($this->address, $this->port);

        $this->server->addlistener($this->address, $this->port, SWOOLE_SOCK_UDP);
        $this->server->set($this->options);
        $this->server->on('connect',    [$this, 'onConnect']);
        $this->server->on('close',      [$this, 'onClose']);
        $this->server->on('start',      [$this, 'onStart']);
        $this->server->on('shutdown',   [$this, 'onShutdown']);
        $this->server->on('WorkerStart',[$this, 'onWorkerStart']);
        $this->server->on('receive',    [$this, 'onReceive']);
        $this->server->on('task',       [$this, 'onTask']);
        $this->server->on('finish',     [$this, 'onFinish']);

        $this->server->addProcess(new \swoole_process(function () {
            $this->server->tick($this->timer, function () {
                \HC\Swoole\Timer::start();
            });
        }));
    }

    /**
     * @param LoggerInterface $logger
     * @return $this
     */
    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;
        return $this;
    }

    /**
     * @return LoggerInterface
     */
    public function getLogger()
    {
        if (!$this->logger) {
            $this->logger = new Logger();
        }

        return $this->logger;
    }

    public function getServer() : \swoole_server
    {
        return $this->server;
    }

    public function start()
    {
        $this->getServer()->start();
    }

    public function onConnect(\swoole_server $server, $client)
    {
        $this->server = $server;
        $this->getLogger()->debug(sprintf('Client %s Connect.', $client));
    }

    public function onClose(\swoole_server $server, $client)
    {
        $this->server = $server;
        $this->getLogger()->debug(sprintf('Client %s Close.', $client));
    }

    public function onStart(\swoole_server $server)
    {
        $pid = $server->master_pid;

        if ($pid_file = static::GetPidFile($this->server_name)) {
            file_put_contents($pid_file, $pid);
        }

        $this->getLogger()->info(sprintf("Server Start, PID: %d ...", $pid));
        $this->getLogger()->info(sprintf("Listen %s:%d ...", $this->address, $this->port));
    }

    public function onShutdown()
    {
        $pid_file = static::GetPidFile($this->server_name);

        if ($pid_file && file_exists($pid_file)) {
            @unlink($pid_file);
        }

        $this->getLogger()->info('Server Close.');
    }

    public function onWorkerStart(\swoole_server $server, int $work_id)
    {
        $this->getLogger()->info(sprintf('Work %d Start', $work_id));

        global $argv;

        if($work_id >= $server->setting['worker_num']) {
            swoole_set_process_name("php {$argv[0]} task-{$work_id}");
        } else {
            swoole_set_process_name("php {$argv[0]} event-{$work_id}");
        }
    }

    public function onReceive(\swoole_server $server, $client, $from_id, $message)
    {
        $this->server = $server;

        $this->getLogger()->debug("receive message", [
            'client' => $client, 'from_id' => $from_id, 'message' => $message
        ]);

        try {
            $request = new Request($this->server, $client, $from_id, $message);

            $cmd  = $request->cmd();
            $args = $request->args();

            $this->getLogger()->debug("request", ['cmd' => $cmd, 'args' => $args]);

            \HC\Swoole\sw_class_exists($cmd);

            if (!in_array(Socket::class, array_values(class_parents($cmd)))) {
                throw new CommandException($cmd . ' is not extend ' . Socket::class);
            }

            $class = new $cmd($request);

            $response = call_user_func_array([$class, 'execute'], $args);
            $response = is_array($response) ? \HC\Swoole\normalizeArray($response) : [];

            $this->send($client, $response);
        } catch (\Throwable $e) {
            $this->exception($client, $e);
        }
    }

    public function onTask(\swoole_server $server, $task_id, $from_id, $data)
    {
        $this->server = $server;

        $this->getLogger()->debug("task start", [
            'task_id' => $task_id, 'from_id' => $from_id, 'data' => $data
        ]);
    }

    public function onFinish(\swoole_server $server, $task_id, $data)
    {
        $this->server = $server;

        $this->getLogger()->debug("task finish", [
            'task_id' => $task_id, 'data' => $data
        ]);
    }

    public function send($client, $data = null)
    {
        $response = [
            'errno'      => '0',
            'error'      => '',
            'client'     => $client,
            'serverTime' => time(),
            'data'       => $data ?: []
        ];

        $this->server->send(
            $client, json_encode($response, JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT)
        );
    }

    public function exception($client, \Throwable $exception)
    {
        $response = [
            'errno'      => $exception->getCode() ?: '10000',
            'error'      => $exception->getMessage() ?: 'server error',
            'client'     => $client,
            'serverTime' => time(),
            'trace'      => explode("\n", $exception->getTraceAsString()),
        ];

        $this->getLogger()->error($exception->getMessage(), $response);

        $this->server->send(
            $client, json_encode($response, JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT)
        );
    }

    /**
     * @param string $server_name
     * @return string
     */
    static public function GetPidFile(string $server_name) : string
    {
        return sprintf("/tmp/hc-sw-%s.pid", strtolower($server_name));
    }
}
